﻿using System;
using System.Collections.Generic;
using System.Text;
using System.Timers;
using System.Linq;

namespace PDO.Utilities
{
    /// <summary>
    /// An expirable, thread-safe synchronized collection.
    /// Size of collection is controlled by expiring unused elements after specified time interval.
    /// 
    /// Design based on Microsoft Best Practices as outlined in...
    /// http://msdn2.microsoft.com/en-us/library/ms978504.aspx#cachingarchch6_designingacustomcache
    /// </summary>
    /// <typeparam name="TKey">key Type</typeparam>
    /// <typeparam name="TValue">value Type</typeparam>
    abstract public class ExpirableCache<TKey, TValue> : SynchronizedKeyedCollection<TKey, CacheItem<TValue>>
    //abstract public class ExpirableCache<TKey, TValue> : Dictionary<TKey, CacheItem<TValue>>
    {
        #region Private Constants and Members

        private const int PurgeTimerMillisecondInterval = 100;

        private Timer _cachePurgeTimer;
        private int _defaultItemExpirationSeconds;

        public Object SyncRoot { get; set; }

        #endregion

        /// <summary>
        /// Constructor
        /// </summary>
        public ExpirableCache()
            : base()
        {
            SyncRoot = new object();
            DefaultItemExpirationSeconds = 60;

            _cachePurgeTimer = new Timer(PurgeTimerMillisecondInterval);
            _cachePurgeTimer.Elapsed += new ElapsedEventHandler(_cachePurgeTimer_Elapsed);
            _cachePurgeTimer.Start();
        }

        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="expirationSeconds">default value for item expiration.  
        /// May be overriden for individual items when using Add().</param>
        public ExpirableCache(int expirationSeconds)
            : this()
        {
            DefaultItemExpirationSeconds = expirationSeconds;
        }


        /// <summary>
        /// Find and remove expired items in the internal cache.
        /// </summary>        
        private void _cachePurgeTimer_Elapsed(object sender, ElapsedEventArgs e)
        {
            _cachePurgeTimer.Stop();

            List<CacheItem<TValue>> expiredItems = new List<CacheItem<TValue>>();

            //Lock the syncroot to prevent the item from being removed from underneath us.
            using (TimedLock.Lock(SyncRoot))
            {
                //foreach (CacheItem<TValue> ci in Values.ToArray())
                foreach (CacheItem<TValue> ci in this.ToArray())
                {
                    if (ci.HasExpired)
                    {
                        base.Remove(ci);
                        //this.Remove(GetKeyForItem(ci));
                        expiredItems.Add(ci);
                    }
                }
            }

            if (expiredItems.Count > 0)
            {
                EventsHelper.Fire<CacheItemsExpiredEventArgs<TValue>>(this.CacheItemsExpired, this, new CacheItemsExpiredEventArgs<TValue>()
                {
                    CacheItems = expiredItems.ToArray()
                });
            }

            _cachePurgeTimer.Start();
        }


        public int DefaultItemExpirationSeconds
        {
            get { return _defaultItemExpirationSeconds; }
            set { _defaultItemExpirationSeconds = value; }
        }

        public CacheItem<TValue> CreateCacheItem()
        {
            return CreateCacheItem(default(TValue));
        }
        public CacheItem<TValue> CreateCacheItem(TValue item)
        {
            return CreateCacheItem(item, DefaultItemExpirationSeconds);
        }
        public CacheItem<TValue> CreateCacheItem(TValue item, int expireSeconds)
        {
            return new CacheItem<TValue>(item, expireSeconds);
        }

        public bool Contains(TValue item)
        {
            return base.Contains(GetKeyForItem(item));
        }

        /// <summary>
        /// Add new element to collection using default expiration interval.
        /// </summary>
        /// <param name="item">element to add</param>
        public void Add(TValue item)
        {
            CacheItem<TValue> ci = CreateCacheItem(item);
            base.Add(ci);
            //Add(GetKeyForItem(ci), ci);
        }


        /// <summary>
        /// Add new element to collection using specified expiration interval.
        /// </summary>
        /// <param name="item">element to add</param>
        /// <param name="expireSeconds">expiration interval in seconds. 0 is immediate, -1 is indefinite.</param>
        public void Add(TValue item, int expireSeconds)
        {
            CacheItem<TValue> ci = new CacheItem<TValue>(item, expireSeconds);
            base.Add(ci);
            //Add(GetKeyForItem(ci), ci);
        }

        /// <summary>
        /// Retreive collection element by key.
        /// </summary>
        /// <param name="key">key value</param>
        /// <returns>collection element</returns>
        public new TValue this[TKey key]
        {
            get
            {
                CacheItem<TValue> ci = base[key];
                ci.LastUsed = DateTime.Now;
                return ci.Item;
            }
            set
            {
                CacheItem<TValue> ci = base[key];
                ci.LastUsed = DateTime.Now;
                ci.Item = value;
            }
        }

        /// <summary>
        /// must be overriden in derived class
        /// </summary>
        /// <param name="item">item</param>
        /// <returns>key</returns>
        protected override TKey GetKeyForItem(CacheItem<TValue> item)
        {
            return GetKeyForItem(item.Item);
        }
        protected abstract TKey GetKeyForItem(TValue item);


        #region Events
        public event EventHandler<CacheItemsExpiredEventArgs<TValue>> CacheItemsExpired;
        #endregion
    }


    public class CacheItem<T>
    {
        #region Private members
        private T _cacheItem;
        private TimeSpan _expirationTime;
        private DateTime _lastUsed;
        #endregion

        /// <summary>
        /// Create a cache item with specified expiration time
        /// </summary>
        /// <param name="cacheItem">item to cache</param>
        /// <param name="expireSeconds">expiration time - seconds. 0 is immediate, -1 is indefinate.</param>
        public CacheItem(T cacheItem, int expireSeconds)
        {
            InitalizeCacheItem(cacheItem, expireSeconds);
        }

        /// <summary>
        /// Time this item will expire.
        /// </summary>
        public DateTime LastUsed
        {
            get { return _lastUsed; }
            set { _lastUsed = value; }
        }

        /// <summary>
        /// Time interval after last access this item will expire.
        /// </summary>
        public TimeSpan ExpirationTime
        {
            get { return _expirationTime; }
            set { _expirationTime = value; }
        }

        /// <summary>
        /// Item in collection
        /// </summary>
        public T Item
        {
            get { return _cacheItem; }
            set { _cacheItem = value; }
        }

        /// <summary>
        /// True if this item has expired.
        /// </summary>
        public bool HasExpired
        {
            get
            {
                return LastUsed + ExpirationTime < DateTime.Now;
            }
        }

        #region Private Methods

        /// <summary>
        /// Initialize the cache item
        /// </summary>
        /// <param name="cacheItem"></param>
        /// <param name="expireSeconds">the number of seconds for the object to export. -1 is replaced with Int.MaxValue</param>
        private void InitalizeCacheItem(T cacheItem, int expireSeconds)
        {
            _cacheItem = cacheItem;
            if (expireSeconds == -1)
            {
                expireSeconds = int.MaxValue;
            }
            ExpirationTime = TimeSpan.FromSeconds(expireSeconds);
            LastUsed = DateTime.Now;
        }

        #endregion
    }

    public class CacheItemsExpiredEventArgs<TValue> : EventArgs
    {
        public CacheItem<TValue>[] CacheItems { get; set; }
    }


}
